package com.fredhat.gwt.xmlrpc.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.VerticalPanel;

/**
 * A live end-to-end example of XMLRPC-GWT code running with the built-in 
 * server side proxy.
 */
public class XMLRPC implements EntryPoint {
	private static final String SERVER_URL = GWT.getModuleBaseURL()+"proxy";
	private static final int TIMEOUT = 30000;
	
	private VerticalPanel testResultsPanel = new VerticalPanel();
	private Timer testTimer;
	private int readyTestCounter = 0;
	private int runningTestCounter = -1;
	private long testStartTime = 0;
	private int passed = 0;
	private int failed = 0;
	private int timedOut = 0;

	/*
	 * Unit tests built here.  Displaying them first since they are the real players
	 * in this class.  Each buildTestCaseOne takes the following parameters:
	 * method name, parameter list, and expected result.
	 */
	
	private TestCase testAddTwo() {
		return buildTestCaseOne("examples.addtwo", new Object[]{3,4}, 7);
	}

	private TestCase testGetStateName() {
		String methodName = "examples.getStateName";
		Object[] params = new Object[]{new Integer(13)};
		String expected = "Idaho";
		return buildTestCaseOne(methodName, params, expected);
	}

	private TestCase testAddTwoDouble() {
		String methodName = "examples.addtwodouble";
		final double d1 = 23.38;
		final double d2 = 83.203;
		Object[] params = new Object[]{d1, d2};
		return buildTestCaseOne(methodName, params, d1+d2);
	}
	
	private TestCase testStringEcho() {
		String methodName = "examples.stringecho";
		final String str = "Testing.";
    	Object[] params = new Object[]{str};
    	return buildTestCaseOne(methodName, params, str);
	}
	
	private TestCase testDecode64() {
		String methodName = "examples.decode64";
    	String encoded = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpc"+
    			"yByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSB"+
    			"vdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsI"+
    			"HRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29"+
    			"udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd"+
    			"2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGN"+
    			"hcm5hbCBwbGVhc3VyZS4=";
    	final String decoded = "Man is distinguished, not only by his reason, but"+
    			" by this singular passion from other animals, which is a l"+
    			"ust of the mind, that by a perseverance of delight in the "+
    			"continued and indefatigable generation of knowledge, excee"+
    			"ds the short vehemence of any carnal pleasure.";
    	Object[] params = new Object[]{new Base64(encoded)};
    	return buildTestCaseOne(methodName, params, decoded);
	}
	
	private TestCase testInvertBooleans() {
		String methodName = "examples.invertBooleans";
    	List<Boolean> inList = new ArrayList<Boolean>();
    	inList.add(true);
    	inList.add(false);
    	inList.add(true);
    	final List<Boolean> outList = new ArrayList<Boolean>();
    	outList.add(false);
    	outList.add(true);
    	outList.add(false);

    	Object[] params = new Object[]{inList};
    	return buildTestCaseOne(methodName, params, outList);
	}
	
	private TestCase testArrayOfStructs() {
		String methodName = "validator1.arrayOfStructsTest";
    	List<Map<String,Integer>> list = new ArrayList<Map<String,Integer>>();
    	Map<String,Integer> map1 = new HashMap<String,Integer>();
    	map1.put("moe", 1);
    	map1.put("larry", 2);
    	map1.put("curly", 4);
    	Map<String,Integer> map2 = new HashMap<String,Integer>();
    	map2.put("moe", 8);
    	map2.put("larry", 16);
    	map2.put("curly", 32);
    	Map<String,Integer> map3 = new HashMap<String,Integer>();
    	map3.put("moe", 64);
    	map3.put("larry", 128);
    	map3.put("curly", 256);
    	list.add(map1);
    	list.add(map2);
    	list.add(map3);

    	Object[] params = new Object[]{list};
    	return buildTestCaseOne(methodName, params, 4+32+256);
	}
	
	private TestCase testEasyStructTest() {
		String methodName = "validator1.easyStructTest";
		int moeValue = 5;
		int larryValue = 7;
		int curlyValue = 9;
		final int sum = moeValue + larryValue + curlyValue;

    	Map<String,Integer> map1 = new HashMap<String,Integer>();
    	map1.put("moe", moeValue);
    	map1.put("larry", larryValue);
    	map1.put("curly", curlyValue);

    	Object[] params = new Object[]{map1};
    	return buildTestCaseOne(methodName, params, sum);
	}
	
	private TestCase testNestedStructTestRead() {
		String methodName = "validator1.nestedStructTest";
		int moeValue = 17;
		int larryValue = 22;
		int curlyValue = 8;
		final int sum = moeValue + larryValue + curlyValue;
		
		Map<String,Integer> stoogesMap = new HashMap<String,Integer>();
		stoogesMap.put("moe", moeValue);
		stoogesMap.put("larry", larryValue);
		stoogesMap.put("curly", curlyValue);

		Map<String,Map<String,Integer>> dayMap = 
				new HashMap<String,Map<String,Integer>>();
		dayMap.put("01", stoogesMap);
		
		Map<String,Map<String,Map<String,Integer>>> monthMap = 
				new HashMap<String,Map<String,Map<String,Integer>>>();
		monthMap.put("04", dayMap);
		
		Map<String,Map<String,Map<String,Map<String,Integer>>>> yearMap = 
				new HashMap<String,Map<String,Map<String,Map<String,Integer>>>>();	
		yearMap.put("2000", monthMap);
		
		Object[] params = new Object[]{yearMap};
		return buildTestCaseOne(methodName, params, sum);
	}
	
	private TestCase testNestedStructEcho() {
		String methodName = "validator1.echoStructTest";
		List<String> goodStooges = new ArrayList<String>();
		goodStooges.add("Moe");
		goodStooges.add("Larry");
		goodStooges.add("Curly");
		
		List<String> badStooges = new ArrayList<String>();
		badStooges.add("Joe");
		badStooges.add("Shemp");
		
		Map<String,List<String>> innerStruct = new HashMap<String,List<String>>();
		innerStruct.put("good", goodStooges);
		innerStruct.put("bad", badStooges);
		
		final Map<String,Map<String,List<String>>> outerStruct = 
				new HashMap<String,Map<String,List<String>>>();
		outerStruct.put("all", innerStruct);
		
		Object[] params = new Object[]{outerStruct};
		return buildTestCaseOne(methodName, params, outerStruct);
	}
	
	private TestCase testNestedStructEcho2() {
		String methodName = "validator1.echoStructTest";
		List<String> stooges = new ArrayList<String>();
		stooges.add("Moe");
		stooges.add("Larry");
		stooges.add("Curly");
		
		final Map<String,List<String>> struct = new HashMap<String,List<String>>();
		struct.put("stooges", stooges);
		
		Object[] params = new Object[]{struct};
		return buildTestCaseOne(methodName, params, struct);
	}
	
	private TestCase testEchoDate() {
		String methodName = "interopEchoTests.echoDate";
    	final com.fredhat.gwt.xmlrpc.client.Date date =
    		new com.fredhat.gwt.xmlrpc.client.Date();
    	Object[] params = new Object[]{date};
    	return buildTestCaseOne(methodName, params, date);
	}
	
	// Attaches all of the above test cases into a list
	private List<TestCase> buildTestCases() {
		List<TestCase> list = new ArrayList<TestCase>();
		list.add(testAddTwo());
		list.add(testGetStateName());
		list.add(testAddTwoDouble());
		list.add(testStringEcho());
		list.add(testDecode64());
		list.add(testInvertBooleans());
		list.add(testArrayOfStructs());
		list.add(testEasyStructTest());
		list.add(testNestedStructTestRead());
		list.add(testNestedStructEcho());
		list.add(testNestedStructEcho2());
		list.add(testEchoDate());
		return list;
	}

	/**
	 * This is the entry point method.
	 */
	public void onModuleLoad() {
		VerticalPanel mainVPanel = new VerticalPanel();
		final Button testButton = new Button("Execute Tests");
		
		final List<TestCase> testCases = buildTestCases();

		mainVPanel.add(testButton);
		mainVPanel.add(testResultsPanel);
		RootPanel.get().add(mainVPanel);
    
		testButton.addClickHandler(new ClickHandler() {
			public void onClick(ClickEvent event) {
				testButton.setEnabled(false);
				testTimer = new Timer() {
					public void run() {
						if (readyTestCounter >= testCases.size()) {
							testTimer.cancel();
							testButton.setEnabled(true);
							testResultsPanel.add(new Label(printFinalResults(testCases.size())));
							return;
						}
						
						if (readyTestCounter > runningTestCounter) {
							// Execute the next test in the list
							XmlRpcClient client = new XmlRpcClient(SERVER_URL);
							client.setTimeoutMillis(25000);
							TestCase testCase = testCases.get(readyTestCounter);
							XmlRpcRequest<Object> request = new XmlRpcRequest<Object>(client, 
									testCase.methodName, testCase.args, testCase.callback);			
							request.execute();
							runningTestCounter = readyTestCounter;
							testStartTime = System.currentTimeMillis();
							GWT.log("Starting test "+runningTestCounter+" at "+testStartTime, null);
						}
						
						if (System.currentTimeMillis() > testStartTime + TIMEOUT) {
							// Test timed out
							GWT.log("Test timed out", null);
							TestCase testCase = testCases.get(runningTestCounter);
							testResultsPanel.add(new Label(printTimeout(testCase.methodName)));
							readyTestCounter++;
							timedOut++;
						}
					}
				};
				
				testTimer.scheduleRepeating(1000);				
			}    	
		});
	}
	
	// Formats and prints the result of a particular test
	private String printTestResult(String methodName, Object expected, Object value) {
		if (expected instanceof Throwable) {
			Throwable t = (Throwable)expected;
			return methodName+" - ERROR: "+t.getMessage();
		}
		
		if (expected.equals(value)) {
			return methodName+" - SUCCESS";
		}
		
		return methodName+" - FAIL: Expected ["+expected+"] but got ["+value+"]";
	}
	
	// Prints the result of a test that timed out
	private String printTimeout(String methodName) {
		return methodName+" - TIMEOUT";
	}
	
	// Prints the final results summary
	private String printFinalResults(int totalCases) {
		StringBuffer sb = new StringBuffer();
		sb.append("Total tests: ").append(totalCases);
		if (passed > 0)
			sb.append(", Passed: ").append(passed);
		if (failed > 0)
			sb.append(", Failed: ").append(failed);
		if (timedOut > 0)
			sb.append(", Timed Out: ").append(timedOut);
		
		return sb.toString();
	}

	// Builds a test case with cookie-cutter success and failure handlers
	private TestCase buildTestCaseOne(final String methodName, Object[] args, 
			final Object expected) {
		return new TestCase(methodName, args, new AsyncCallback<Object>() {
			public void onSuccess(Object response) {
				testResultsPanel.add(new Label(printTestResult(methodName, expected, response)));
				readyTestCounter++;
				passed++;
			}
			
			public void onFailure(Throwable response) {
				testResultsPanel.add(new Label(printTestResult(methodName, response, null)));
				readyTestCounter++;
				failed++;
			}
		});
	}
		
	/*
	 * Simple test case POJO to better track each individual test case
	 */
	private class TestCase {
		private String methodName;
		private Object[] args;
		private AsyncCallback<Object> callback;
		
		public TestCase(String methodName, Object[] args, AsyncCallback<Object> callback) {
			this.methodName = methodName;
			this.args = args;
			this.callback = callback;
		}
	}
}
